/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package chain

import (
	reqContext "context"
	"time"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/client/chain/invoke"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/retry"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/status"
	contextImpl "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain_sdk/metrics"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"github.com/pkg/errors"
)

// Client
// @Description: 链客户端对象，负责调用合约
type Client struct {
	chainCtx     context.Chain          // 链对象
	eventService ichain.EventService    // 事件订阅
	metrics      *metrics.ClientMetrics // 指标统计
}

// New
// @Description: 创建链客户端
// @param chainProvider 创建链对象，提供链服务
// @return *Client
// @return error
func New(chainProvider context.ChainProvider) (*Client, error) {

	// 创建链对象
	chainContext, err := chainProvider()
	if err != nil {
		return nil, errors.WithMessage(err, "failed to create chain context")
	}

	// 获取链服务
	chainService := chainContext.ChainService()

	if chainService == nil {
		return nil, errors.New("chain service not initialized")
	}

	// 创建事件订阅服务
	eventService, err := chainService.EventService()
	if err != nil {
		return nil, errors.WithMessage(err, "event service creation failed")
	}

	// 创建链客户端
	chainClient := newClient(chainContext, eventService)
	return &chainClient, nil
}

func (cc *Client) Query(request Request, options ...RequestOption) (Response, error) {
	options = append(options, addDefaultTimeout(ichain.Query))
	return callQuery(cc, request, options...)
}

func (cc *Client) Execute(request Request, options ...RequestOption) (Response, error) {
	options = append(options, addDefaultTimeout(ichain.Execute))
	return callExecute(cc, request, options...)
}

func addDefaultTimeout(tt ichain.TimeoutType) RequestOption {
	return func(ctx context.Client, o *requestOptions) error {
		if o.Timeouts[tt] == 0 {
			return WithTimeout(tt, ctx.EndpointConfig().Timeout(tt))(ctx, o)
		}
		return nil
	}
}

func (cc *Client) InvokeHandler(handler invoke.Handler, request Request, options ...RequestOption) (Response, error) {
	txnOpts, err := cc.prepareOptsFromOptions(cc.chainCtx, options...)
	if err != nil {
		return Response{}, err
	}

	reqCtx, cancel := cc.createReqContext(&txnOpts)
	defer cancel()

	requestContext, clientContext, err := cc.prepareHandlerContexts(reqCtx, request, txnOpts)
	if err != nil {
		return Response{}, err
	}

	invoker := retry.NewInvoker(
		requestContext.RetryHandler,
		retry.WithBeforeRetry(
			func(err error) {
				if requestContext.Opts.BeforeRetry != nil {
					requestContext.Opts.BeforeRetry(err)
				}

				requestContext.Opts.Targets = txnOpts.Targets
				requestContext.Error = nil
				requestContext.Response = invoke.Response{}
			},
		),
	)

	complete := make(chan bool, 1)
	go func() {
		_, _ = invoker.Invoke(
			func() (interface{}, error) {
				handler.Handle(requestContext, clientContext)
				return nil, requestContext.Error
			})
		complete <- true
	}()
	select {
	case <-complete:
		return Response(requestContext.Response), requestContext.Error
	case <-reqCtx.Done():
		return Response{}, status.New(status.ClientStatus, status.Timeout.ToInt32(),
			"request timed out or been cancelled", nil)
	}
}

func (cc *Client) createReqContext(txnOpts *requestOptions) (reqContext.Context, reqContext.CancelFunc) {

	if txnOpts.Timeouts == nil {
		txnOpts.Timeouts = make(map[ichain.TimeoutType]time.Duration)
	}

	if txnOpts.Timeouts[ichain.Execute] == 0 {
		txnOpts.Timeouts[ichain.Execute] = cc.chainCtx.EndpointConfig().Timeout(ichain.Execute)
	}

	reqCtx, cancel := contextImpl.NewRequest(cc.chainCtx, contextImpl.WithTimeout(txnOpts.Timeouts[ichain.Execute]),
		contextImpl.WithParent(txnOpts.ParentContext))
	reqCtx = reqContext.WithValue(reqCtx, contextImpl.ReqContextTimeoutOverrides, txnOpts.Timeouts)

	return reqCtx, cancel
}

func (cc *Client) prepareHandlerContexts(reqCtx reqContext.Context, request Request, o requestOptions) (*invoke.RequestContext, *invoke.ClientContext, error) {

	if request.ContractID == "" || request.Fcn == "" {
		return nil, nil, errors.New("ContractID and Fcn are required")
	}

	transactor, err := cc.chainCtx.ChainService().Transactor(reqCtx)
	if err != nil {
		return nil, nil, errors.WithMessage(err, "failed to create transactor")
	}

	discovery, err := cc.chainCtx.ChainService().Discovery()
	if err != nil {
		return nil, nil, errors.WithMessage(err, "failed to create discovery service")
	}

	member, err := cc.chainCtx.ChainService().Membership()
	if err != nil {
		return nil, nil, errors.WithMessage(err, "failed to get membership")
	}

	// 构建请求上下文
	clientContext := &invoke.ClientContext{
		Discovery:    discovery,
		Membership:   member,
		Transactor:   transactor,
		EventService: cc.eventService,
	}

	requestContext := &invoke.RequestContext{
		Request:      invoke.Request(request),
		Opts:         invoke.Opts(o),
		Response:     invoke.Response{},
		RetryHandler: retry.New(o.Retry),
		Ctx:          reqCtx,
	}

	return requestContext, clientContext, nil
}

func (cc *Client) prepareOptsFromOptions(ctx context.Client, options ...RequestOption) (requestOptions, error) {
	txnOpts := requestOptions{}
	for _, option := range options {
		err := option(ctx, &txnOpts)
		if err != nil {
			return txnOpts, errors.WithMessage(err, "Failed to read opts")
		}
	}
	return txnOpts, nil
}

func (cc *Client) RegisterContractEvent(chainCodeID string, eventFilter string) (ichain.Registration, <-chan *ichain.ContractEvent, error) {
	return cc.eventService.RegisterContractEvent(chainCodeID, eventFilter)
}

func (cc *Client) UnregisterContractEvent(registration ichain.Registration) {
	cc.eventService.Unregister(registration)
}

func newClient(chainContext context.Chain, eventService ichain.EventService) Client {
	chainClient := Client{
		eventService: eventService,
		chainCtx:     chainContext,
		metrics:      chainContext.GetMetrics(),
	}
	return chainClient
}
